// **********************************************************************
// 
// <copyright>
// 
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
// 
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
// 
// </copyright>
// **********************************************************************
// 
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/gui/ToolPanel.java,v $
// $RCSfile: ToolPanel.java,v $
// $Revision: 1.13 $
// $Date: 2006/03/06 15:41:47 $
// $Author: dietrick $
// 
// **********************************************************************

package com.bbn.openmap.gui;

import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.beancontext.BeanContext;
import java.beans.beancontext.BeanContextChild;
import java.beans.beancontext.BeanContextChildSupport;
import java.beans.beancontext.BeanContextMembershipEvent;
import java.beans.beancontext.BeanContextMembershipListener;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JLabel;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;

import com.bbn.openmap.Environment;
import com.bbn.openmap.I18n;
import com.bbn.openmap.PropertyConsumer;
import com.bbn.openmap.gui.menu.ToolPanelToggleMenuItem;
import com.bbn.openmap.util.PropUtils;

/**
 * Represents the toolbar containing tools to apply to the map. Tools can be
 * added in sequential order, and retrieved using the tool's keyword. NOTE:
 * Every time a string is passed into a method of this class, the interned
 * version of it is used as a key.
 * <P>
 * 
 * When the ToolPanel is part of the BeanContext, it looks for Tools that have
 * also been added to the BeanContext. If there is more than one ToolPanel in a
 * BeanContext at a time, both will show the same Tool faces. The 'components'
 * property can be used to control which tools can be added to a specific
 * instance of a ToolPanel. That property should contain a space separated list
 * of prefixes used for Tools, which in turn should be set in the Tools as their
 * keys.
 * 
 * @see Tool
 * @author john gash
 */
public class ToolPanel extends JToolBar implements BeanContextChild, BeanContextMembershipListener,
        MapPanelChild, PropertyConsumer, ComponentListener {

    private static Logger logger = Logger.getLogger(ToolPanel.class.getName());
    private static final long serialVersionUID = 1L;
    /** The set of tools contained on the toolbar. */
    protected Hashtable<String, Tool> items = new Hashtable<String, Tool>();
    /**
     * A flag to note whether the ToolPanel inserts spaces between tools.
     */
    protected boolean autoSpace = false;

    /**
     * BeanContextChildSupport object provides helper functions for
     * BeanContextChild interface.
     */
    private BeanContextChildSupport beanContextChildSupport = new BeanContextChildSupport(this);

    /**
     * The property prefix used for this ToolPanel.
     */
    protected String propertyPrefix = null;

    /**
     * A list of components to use for filtering tools found in the MapHandler
     * to add to this ToolPanel.
     */
    public final static String ComponentsProperty = "components";

    /**
     * A list of components to use for filtering out tools found in the
     * MapHandler. Components added to this list will NOT be added to this
     * ToolPanel.
     */
    public final static String AvoidComponentsProperty = "avoid";

    public final static String MembershipProperty = "membership";

    public final static String NameProperty = "name";

    /**
     * A filter list of components to look for and add.
     */
    protected List<String> componentList = null;

    /**
     * A filter list of components to avoid.
     */
    protected List<String> avoidList = null;

    protected GridBagLayout gridbag = new GridBagLayout();
    protected GridBagConstraints c = new GridBagConstraints();
    protected String parentName = null;

    /**
     * Holder that expands in the GridBagLayout, keeping things pushed to the
     * left side of the toolpanel.
     */
    protected JLabel filler = null;

    /**
     * Constructor
     */
    public ToolPanel() {
        setLayout(gridbag);
        setFloatable(false);
        setVisible(false);
        setName("Tool Panel");
    }

    /**
     * Add an item to the tool bar.
     * 
     * @param key The key associated with the item.
     * @param item The Tool to add.
     */
    public void add(String key, Tool item) {
        add(key, item, -1);
    }

    /**
     * A little array used to track what indexes are already used, to prevent
     * the GridBagLayout from placing things on top of each other.
     */
    protected boolean[] usedIndexes;
    public int MAX_INDEXES = 101;

    /**
     * Provides the next available component index for placement, starting at 0.
     */
    protected int getNextAvailableIndex() {
        return getNextAvailableIndex(0);
    }

    /**
     * Provides the next available component index for placement, given a
     * starting index.
     */
    protected int getNextAvailableIndex(int startAt) {

        if (usedIndexes == null) {
            usedIndexes = new boolean[MAX_INDEXES];
        }

        if (startAt < 0)
            startAt = 0;
        if (startAt >= MAX_INDEXES)
            startAt = MAX_INDEXES - 1;

        int i = startAt;
        // Find the first unused
        for (; i < MAX_INDEXES && usedIndexes[i] == true; i++) {
        }
        usedIndexes[i] = true;

        return i;
    }

    /**
     * Add an item to the tool bar.
     * 
     * @param key The key associated with the item.
     * @param item The Tool to add.
     * @param index The position index for placement of the tool. -1 puts it at
     *        the end, and if the position is greater than the size, it is
     *        placed at the end. This class does not remember where items were
     *        asked to be placed, so later additions may mess up intended order.
     */
    public void add(String key, Tool item, int index) {

        int orientation = getOrientation();
        boolean hOrient = orientation == SwingConstants.HORIZONTAL;
        item.setOrientation(orientation);

        Container face = item.getFace();

        if (face != null) {
            face.addComponentListener(this);
            items.put(key.intern(), item);

            if (autoSpace) {
                index *= 2;
            }

            if (hOrient) {
                c.weightx = 0;
                c.gridx = getNextAvailableIndex(index);
                c.gridy = 0;
                c.anchor = GridBagConstraints.WEST;
            } else {
                c.weighty = 0;
                c.gridx = 0;
                c.gridy = getNextAvailableIndex(index);
                c.anchor = GridBagConstraints.NORTH;
            }

            gridbag.setConstraints(face, c);
            add(face);

            if (filler == null) {
                if (hOrient) {
                    c.gridx = getNextAvailableIndex(MAX_INDEXES);
                    c.anchor = GridBagConstraints.EAST;
                    c.weightx = 1;
                } else {
                    c.gridy = getNextAvailableIndex(MAX_INDEXES);
                    c.anchor = GridBagConstraints.SOUTH;
                    c.weighty = 1;
                }

                filler = new JLabel("");
                gridbag.setConstraints(filler, c);
                add(filler);
            }

            if (autoSpace) {
                JLabel l = new JLabel(" ");
                gridbag.setConstraints(l, c);
                add(l);
            }
        }

        setVisibility();
        firePropertyChange(MembershipProperty, null, items);
    }

    /**
     * Add an item to the tool bar. Assumes that the key will be picked out of
     * the Tool.
     * 
     * @param item The Tool to add.
     */
    public void add(Tool item) {
        add(item, -1);
    }

    /**
     * Add an item to the tool bar. Assumes that the key will be picked out of
     * the Tool.
     * 
     * @param item The Tool to add.
     * @param index The position to add the Tool. -1 will add it to the end.
     */
    public void add(Tool item, int index) {
        try {
            add(item.getKey(), item, index);
        } catch (NullPointerException npe) {
            if (item != null) {
                logger.warning("ToolPanel.add(): no name for " + item.getClass().getName());
                npe.printStackTrace();
            } else {
                logger.warning("ToolPanel.add(): no name for null tool.");
            }
        }
    }

    /**
     * Get an item from the tool bar.
     * 
     * @param key The key associated with the item.
     * @return The tool associated with the key, null if not found.
     */
    public Tool get(String key) {
        return (Tool) items.get(key.intern());
    }

    /** Remove a tool with the right key */
    public void remove(String key) {
        Tool tool = (Tool) items.remove(key.intern());
        if (tool != null) {
            remove(tool.getFace());
            tool.getFace().removeComponentListener(this);
            firePropertyChange(MembershipProperty, null, items);
        }
    }

    /** Add a space between tools. */
    protected void addSpace() {
        add(new JLabel(" "));
    }

    /** Set whether spaces are placed between tools. */
    public void setAutoSpace(boolean set) {
        autoSpace = set;
    }

    /**
     * BorderLayout.NORTH by default for this class.
     */
    protected String preferredLocation = java.awt.BorderLayout.NORTH;

    /**
     * MapPanelChild method.
     */
    public void setPreferredLocation(String value) {
        preferredLocation = value;
    }

    /** MapPanelChild method. */
    public String getPreferredLocation() {
        return preferredLocation;
    }

    /** Find out whether spaces are being placed between tools. */
    public boolean isAutoSpace() {
        return autoSpace;
    }

    /**
     * Set the list of strings used by the ToolPanel to figure out which Tools
     * should be added (in the findAndInit()) method and where they should go.
     */
    public void setComponentList(List<String> list) {
        componentList = list;
    }

    /**
     * Get the list of strings used by the ToolPanel to figure out which Tools
     * should be added (in the findAndInit()) method and where they should go.
     */
    public List<String> getComponentList() {
        return componentList;
    }

    /**
     * Set the list of strings used by the ToolPanel to figure out which Tools
     * should not be added (in the findAndInit()) method.
     */
    public void setAvoidList(List<String> list) {
        avoidList = list;
    }

    /**
     * Get the list of strings used by the ToolPanel to figure out which Tools
     * should not be added (in the findAndInit()) method.
     */
    public List<String> getAvoidList() {
        return avoidList;
    }

    /**
     * Called when the ToolPanel is added to the BeanContext, and when new
     * objects are added to the BeanContext after that. The ToolPanel looks for
     * Tools that are part of the BeanContext.
     * 
     * @param it iterator to use to go through the new objects.
     */
    public void findAndInit(Iterator<Object> it) {
        while (it.hasNext()) {
            findAndInit(it.next());
        }
    }

    /**
     * Figure out if the string key is in the provided list, and provide the
     * location index of it is.
     * 
     * @param key the key of the component to check for.
     * @param list the list of keys to check.
     * @return -1 if not on the list, the index starting at 0 if it is.
     */
    protected int keyOnList(String key, List<String> list) {
        int ret = -1;
        int index = 0;
        if (list != null) {
            for (String listKey : list) {
                if (listKey.equalsIgnoreCase(key)) {
                    ret = index;
                    break;
                }
                index++;
            }
        }
        return ret;
    }

    public void findAndInit(Object someObj) {
        if (someObj instanceof Tool) {
            String key = ((Tool) someObj).getKey();
            List<String> list = getComponentList();
            int index;
            if (list != null) {
                index = keyOnList(key, list);

                if (index >= 0) {
                    if (logger.isLoggable(Level.FINE)) {
                        logger.log(Level.FINE, "ToolPanel: found a tool Object {0} for placement at {1}", new Object[] {
                                key, index });
                    }

                    add((Tool) someObj, index);
                }

            } else {
                index = keyOnList(key, getAvoidList());
                if (index < 0) {
                    logger.fine("ToolPanel: found a tool Object");
                    add((Tool) someObj);
                }
            }
        }
    }

    /**
     * Get a menu item that controls the visibility of this ToolPanel.
     * 
     * @return ToolPanelToggleMenuItem
     */
    public ToolPanelToggleMenuItem getToggleMenu() {
        return new ToolPanelToggleMenuItem(this);
    }

    /**
     * Checks to see if the menu item controls this ToolPanel.
     * 
     * @param mi
     * @return true if menu item refers to this tool panel.
     */
    public boolean checkToolPanelToggleMenuItem(ToolPanelToggleMenuItem mi) {
        return (mi != null && mi.getToolPanel().equals(this));
    }

    /**
     * BeanContextMembershipListener method. Called when objects have been added
     * to the parent BeanContext.
     * 
     * @param bcme the event containing the iterator with new objects.
     */
    public void childrenAdded(BeanContextMembershipEvent bcme) {
        findAndInit(bcme.iterator());
    }

    /**
     * BeanContextMembershipListener method. Called when objects have been
     * removed from the parent BeanContext. If the ToolPanel finds a Tool in the
     * list, it removes it from the ToolPanel.
     * 
     * @param bcme the event containing the iterator with removed objects.
     */
    public void childrenRemoved(BeanContextMembershipEvent bcme) {
        Iterator<Object> it = bcme.iterator();
        Object someObj;
        while (it.hasNext()) {
            someObj = it.next();
            if (someObj instanceof Tool) {
                // do the initializing that need to be done here
                logger.fine("ToolPanel removing tool Object");
                remove(((Tool) someObj).getKey());
            }
        }
    }

    /** Method for BeanContextChild interface. */
    public BeanContext getBeanContext() {
        return beanContextChildSupport.getBeanContext();
    }

    /**
     * Method for BeanContextChild interface. Called when the ToolPanel is added
     * to the BeanContext.
     * 
     * @param in_bc the BeanContext.
     */
    public void setBeanContext(BeanContext in_bc) throws PropertyVetoException {
        if (in_bc != null) {
            in_bc.addBeanContextMembershipListener(this);
            beanContextChildSupport.setBeanContext(in_bc);
            findAndInit(in_bc.iterator());
        }
    }

    /** Method for BeanContextChild interface. */
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener in_pcl) {
        beanContextChildSupport.addPropertyChangeListener(propertyName, in_pcl);
    }

    /** Method for BeanContextChild interface. */
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener in_pcl) {
        beanContextChildSupport.removePropertyChangeListener(propertyName, in_pcl);
    }

    /** Method for BeanContextChild interface. */
    public void addVetoableChangeListener(String propertyName, VetoableChangeListener in_vcl) {
        beanContextChildSupport.addVetoableChangeListener(propertyName, in_vcl);
    }

    /** Method for BeanContextChild interface. */
    public void removeVetoableChangeListener(String propertyName, VetoableChangeListener in_vcl) {
        beanContextChildSupport.removeVetoableChangeListener(propertyName, in_vcl);
    }

    public void setPropertyPrefix(String prefix) {
        propertyPrefix = prefix;
    }

    public String getPropertyPrefix() {
        return propertyPrefix;
    }

    public void setProperties(Properties props) {
        setProperties(null, props);
    }

    public void setProperties(String prefix, Properties props) {
        setPropertyPrefix(prefix);

        prefix = PropUtils.getScopedPropertyPrefix(prefix);

        String componentsString = props.getProperty(prefix + ComponentsProperty);

        if (componentsString != null) {
            setComponentList(PropUtils.parseSpacedMarkers(componentsString));
        }

        String avoidComponentsString = props.getProperty(prefix + AvoidComponentsProperty);

        if (avoidComponentsString != null) {
            setAvoidList(PropUtils.parseSpacedMarkers(avoidComponentsString));
        }

        String preferredLocationString = props.getProperty(prefix + PreferredLocationProperty);

        if (preferredLocationString != null) {
            try {
                preferredLocationString = (String) java.awt.BorderLayout.class.getField(preferredLocationString).get(null);
            } catch (NoSuchFieldException nsfe) {
                preferredLocationString = null;
            } catch (IllegalAccessException iae) {
                preferredLocationString = null;
            }

            if (preferredLocationString != null) {
                setPreferredLocation(preferredLocationString);
                if (preferredLocationString.equalsIgnoreCase("WEST")
                        || preferredLocationString.equalsIgnoreCase("EAST")) {
                    setOrientation(SwingConstants.VERTICAL);
                }
            }
        }

        setName(props.getProperty(prefix + NameProperty, getName()));
        setParentName(props.getProperty(prefix + ParentNameProperty, getParentName()));
    }

    /**
     * Take a List of strings, and return a space-separated version. Return null
     * if the List is null.
     */
    protected StringBuffer rebuildListProperty(List<String> aList) {
        StringBuffer list = null;
        if (aList != null) {
            list = new StringBuffer();
            for (String toolKey : aList) {
                list.append(toolKey).append(" ");
            }
        }
        return list;
    }

    public Properties getProperties(Properties props) {
        if (props == null) {
            props = new Properties();
        }

        String prefix = PropUtils.getScopedPropertyPrefix(this);

        StringBuffer listProp = rebuildListProperty(getComponentList());
        if (listProp != null) {
            props.put(prefix + ComponentsProperty, listProp.toString());
        }

        listProp = rebuildListProperty(getAvoidList());
        if (listProp != null) {
            props.put(prefix + AvoidComponentsProperty, listProp.toString());
        }

        PropUtils.putIfNotDefault(props, prefix + PreferredLocationProperty, getPreferredLocation());
        PropUtils.putIfNotDefault(props, prefix + NameProperty, getName());
        PropUtils.putIfNotDefault(props, prefix + ParentNameProperty, getParentName());
        return props;
    }

    public Properties getPropertyInfo(Properties props) {
        if (props == null) {
            props = new Properties();
        }

        I18n i18n = Environment.getI18n();

        PropUtils.setI18NPropertyInfo(i18n, props, ToolPanel.class, ComponentsProperty, "Tool Names", "List of Names of Tools to Add", null);
        PropUtils.setI18NPropertyInfo(i18n, props, ToolPanel.class, AvoidComponentsProperty, "Avoid Tool Names", "List of Names of Tools to Not Add", null);
        PropUtils.setI18NPropertyInfo(i18n, props, ToolPanel.class, PreferredLocationProperty, "Location", "Preferred Location of Tool Panel", null);
        PropUtils.setI18NPropertyInfo(i18n, props, ToolPanel.class, NameProperty, "Tool Name", "Name of This Tool Panel", null);

        return props;
    }

    /**
     * If any of the components are visible, set the ToolPanel to be visible. If
     * all of them are invisible, make the ToolPanel invisible.
     */
    protected void setVisibility() {
        setVisible(areComponentsVisible());
    }

    public boolean areComponentsVisible() {
        Enumeration<Tool> enumeration = items.elements();
        while (enumeration.hasMoreElements()) {
            Tool tool = enumeration.nextElement();
            Container face = tool.getFace();
            // make sure tool != filler - filler(JPanel) test is for object
            // equivalence
            if (!filler.equals(tool) && face != null && face.isVisible()) {
                return true;
            }
        }
        return false;
    }

    public void componentHidden(ComponentEvent ce) {
        setVisibility();
    }

    public void componentMoved(ComponentEvent ce) {

    }

    public void componentResized(ComponentEvent ce) {

    }

    public void componentShown(ComponentEvent ce) {
        setVisibility();
    }

    public String getParentName() {
        return parentName;
    }

    public void setParentName(String parentName) {
        this.parentName = parentName;
    }

}
